﻿using ComputedTable.Version10;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MortgageCalculator.Demo;
using System;
using System.Collections.Generic;
using System.Linq;

namespace MortgageCalculator.Tests
{
    [TestClass]
    public class MortgageCalculatorTest : ICalculatorFactory<int, string, MortgageCalculatorOperationContext>
    {
        private MortgageCalculator.Demo.MortgageCalculator target;
        private Dictionary<Type, FakeCalculator> dictionary = new Dictionary<Type, FakeCalculator>();

        private MortgageCalculatorInput calculateInput = new MortgageCalculatorInput
        {
            DurationOfLoan = 10,
            InterestRate = .05m,
            LoanAmount = 200000,
            LoanStartDate = new DateTime(2015, 1, 1),
            MaxBalanceAtMiddleDuration = 150000,
            MaxInterestPaidWarning = 100000,
            PropertyTax = 500,
            PurchasePrice = 250000
        };

        private MaxInterestRateCalculatorInput interestRateCalculateInput = new MaxInterestRateCalculatorInput
        {
            DurationOfLoan = 10,
            LoanAmount = 200000,
            LoanStartDate = new DateTime(2015, 1, 1),
            MaxInterestPaid = 100000
        };

        [TestInitialize]
        public void Init()
        {
            this.PrepareDictionary();
            this.target = new Demo.MortgageCalculator(this);
        }

        [TestMethod]
        public void Calculate_GivengInput_Test_SucceedResult()
        {
            var actual = this.target.Calculate(this.calculateInput);
            Assert.IsTrue(actual.Succeed);
            Assert.IsNull(actual.Error);
            Assert.IsNull(actual.Warnings);
        }

        [TestMethod]
        public void Calculate_GivengInput_Test_MonthlyLoanPayments()
        {
            var actual = this.target.Calculate(this.calculateInput);
            Assert.AreEqual(20461m, actual.MonthlyLoanPayments);
        }

        [TestMethod]
        public void Calculate_GivengInput_Test_TotalInterestPaid()
        {
            var interestCalculator = this.Resolve<IInterestCalculator>() as FakeCalculator;
            var currentDelegate = interestCalculator.ExecuteDelegate;
            interestCalculator.ExecuteDelegate = (ctx) =>
            {
                ctx.TotalInterestPaid = 99m;
                return currentDelegate(ctx);
            };

            var actual = this.target.Calculate(this.calculateInput);

            Assert.AreEqual(99m, actual.TotalInterestPaid);
        }

        [TestMethod]
        public void Calculate_GivengInput_Test_AmortizationTable()
        {
            var output = this.target.Calculate(this.calculateInput);

            string expected = @"<table>
  <row PaymentNumber=""1"" RemainingPayments=""9"" PropertyTax=""500"" Interest=""100"" Principal=""200"" OpeningBalance=""500"" ClosingBalance=""400"" PaymentDate=""1/1/2015 12:00:00 AM"" TotalPayments=""300"" />
  <row PaymentNumber=""2"" RemainingPayments=""8"" PropertyTax=""500"" Interest=""101"" Principal=""201"" OpeningBalance=""501"" ClosingBalance=""401"" PaymentDate=""2/1/2015 12:00:00 AM"" TotalPayments=""301"" />
  <row PaymentNumber=""3"" RemainingPayments=""7"" PropertyTax=""500"" Interest=""102"" Principal=""202"" OpeningBalance=""502"" ClosingBalance=""402"" PaymentDate=""3/1/2015 12:00:00 AM"" TotalPayments=""302"" />
  <row PaymentNumber=""4"" RemainingPayments=""6"" PropertyTax=""500"" Interest=""103"" Principal=""203"" OpeningBalance=""503"" ClosingBalance=""403"" PaymentDate=""4/1/2015 12:00:00 AM"" TotalPayments=""303"" />
  <row PaymentNumber=""5"" RemainingPayments=""5"" PropertyTax=""500"" Interest=""104"" Principal=""204"" OpeningBalance=""504"" ClosingBalance=""404"" PaymentDate=""5/1/2015 12:00:00 AM"" TotalPayments=""304"" />
  <row PaymentNumber=""6"" RemainingPayments=""4"" PropertyTax=""500"" Interest=""105"" Principal=""205"" OpeningBalance=""505"" ClosingBalance=""405"" PaymentDate=""6/1/2015 12:00:00 AM"" TotalPayments=""305"" />
  <row PaymentNumber=""7"" RemainingPayments=""3"" PropertyTax=""500"" Interest=""106"" Principal=""206"" OpeningBalance=""506"" ClosingBalance=""406"" PaymentDate=""7/1/2015 12:00:00 AM"" TotalPayments=""306"" />
  <row PaymentNumber=""8"" RemainingPayments=""2"" PropertyTax=""500"" Interest=""107"" Principal=""207"" OpeningBalance=""507"" ClosingBalance=""407"" PaymentDate=""8/1/2015 12:00:00 AM"" TotalPayments=""307"" />
  <row PaymentNumber=""9"" RemainingPayments=""1"" PropertyTax=""500"" Interest=""108"" Principal=""208"" OpeningBalance=""508"" ClosingBalance=""408"" PaymentDate=""9/1/2015 12:00:00 AM"" TotalPayments=""308"" />
  <row PaymentNumber=""10"" RemainingPayments=""0"" PropertyTax=""500"" Interest=""109"" Principal=""209"" OpeningBalance=""509"" ClosingBalance=""409"" PaymentDate=""10/1/2015 12:00:00 AM"" TotalPayments=""309"" />
</table>";
            string actual = output.AmortizationTableAsXml().ToString();
            Assert.AreEqual(expected, actual);
        }

        [TestMethod]
        public void Calculate_GivengInput_Test_MaxInterestPaidWarning()
        {
            var interestCalculator = this.Resolve<IInterestCalculator>() as FakeCalculator;
            var currentDelegate = interestCalculator.ExecuteDelegate;
            interestCalculator.ExecuteDelegate = (ctx) =>
            {
                if (ctx.CurrentCell.RowId == 5)
                {
                    ctx.TotalInterestPaid = calculateInput.MaxInterestPaidWarning + 1;
                }

                return currentDelegate(ctx);
            };

            var actual = this.target.Calculate(this.calculateInput);

            Assert.IsNotNull(actual.Warnings);
            Assert.AreEqual(1, actual.Warnings.Count);
            Assert.AreEqual("Max Interest Paid [$100,000.00] reached at payment number 5", actual.Warnings[0]);
        }

        [TestMethod]
        public void Calculate_GivengInput_Test_MaxBalanceAtMiddleDuration()
        {
            var interestCalculator = this.Resolve<IOpeningBalanceCalculator>() as FakeCalculator;
            var currentDelegate = interestCalculator.ExecuteDelegate;
            int count = 0;
            interestCalculator.ExecuteDelegate = (ctx) =>
            {
                count++;
                return CellOperationResult.CreateSucceed(calculateInput.MaxBalanceAtMiddleDuration + count);
            };

            var actual = this.target.Calculate(this.calculateInput);
            Assert.IsFalse(actual.Succeed);
            Assert.AreEqual("The Opening Balance At Middle Duration [$150,005.00] is greater than Max Balance At Middle Duration [$150,000.00]", actual.Error);
        }

        [TestMethod]
        public void CalculateInterestRateByPaidInterest_GivenTooLowInterestRate_Test_FailedResult()
        {
            var interestCalculator = this.Resolve<IOpeningBalanceCalculator>() as FakeCalculator;
            var currentDelegate = interestCalculator.ExecuteDelegate;
            interestCalculator.ExecuteDelegate = (ctx) =>
            {
                ctx.TotalInterestPaid = this.interestRateCalculateInput.MaxInterestPaid + 1;
                return currentDelegate(ctx);
            };

            var actual = this.target.CalculateInterestRateByPaidInterest(this.interestRateCalculateInput, .01m, .02m);

            Assert.IsFalse(actual.InterestRate.HasValue);
            Assert.AreEqual("Impossible with the interest rate range.", actual.Message);
        }

        [TestMethod]
        public void CalculateInterestRateByPaidInterest_GivenInput_Test_FailedResult()
        {
            var interestCalculator = this.Resolve<IOpeningBalanceCalculator>() as FakeCalculator;
            var currentDelegate = interestCalculator.ExecuteDelegate;
            int count = 0;
            interestCalculator.ExecuteDelegate = (ctx) =>
            {
                count++;
                if (count <= 10)
                {
                    ctx.TotalInterestPaid = 1000; // this will make it pass the first validation
                }
                else
                {
                    ctx.TotalInterestPaid = this.interestRateCalculateInput.MaxInterestPaid + 1; // this will make all the other validation fail.
                }

                return currentDelegate(ctx);
            };

            var actual = this.target.CalculateInterestRateByPaidInterest(this.interestRateCalculateInput, .0001m, 10000000000m);

            Assert.IsFalse(actual.InterestRate.HasValue);
            Assert.AreEqual("Too many iterations.", actual.Message);
        }


        [TestMethod]
        public void CalculateInterestRateByPaidInterest_GivenInput_Test_SucceedResultWithExactMaxInterestPaidFound()
        {
            var interestCalculator = this.Resolve<IOpeningBalanceCalculator>() as FakeCalculator;
            var currentDelegate = interestCalculator.ExecuteDelegate;
            int count = 0;
            List<decimal> interestRates = new List<decimal>();
            interestCalculator.ExecuteDelegate = (ctx) =>
            {
                interestRates.Add(ctx.Input.InterestRate);
                count++;
                // This simulates that the third calculation has the desired TotalInterestPaid
                if (count == this.interestRateCalculateInput.DurationOfLoan * 3)
                {
                    ctx.TotalInterestPaid = this.interestRateCalculateInput.MaxInterestPaid;
                }

                return currentDelegate(ctx);
            };

            var actual = this.target.CalculateInterestRateByPaidInterest(this.interestRateCalculateInput, .02m, .2m);

            decimal expectedInterestRateThirdCalculation = .155m;

            Assert.IsTrue(actual.InterestRate.HasValue);
            Assert.AreEqual(expectedInterestRateThirdCalculation, actual.InterestRate.Value);
            Assert.IsNull(actual.Message);
        }

        [TestMethod]
        public void CalculateInterestRateByPaidInterest_GivenInput_Test_SucceedResult()
        {
            var interestCalculator = this.Resolve<IOpeningBalanceCalculator>() as FakeCalculator;
            var currentDelegate = interestCalculator.ExecuteDelegate;
            int count = 0;
            List<decimal> interestRates = new List<decimal>();
            interestCalculator.ExecuteDelegate = (ctx) =>
            {
                interestRates.Add(ctx.Input.InterestRate);
                count++;

                // This simulates that the second calculation has a low Interest Paid
                if (count == this.interestRateCalculateInput.DurationOfLoan * 2)
                {
                    ctx.TotalInterestPaid = this.interestRateCalculateInput.MaxInterestPaid - 1;
                }

                // This simulates that the third calculation has a high Interest Paid
                if (count >= this.interestRateCalculateInput.DurationOfLoan * 3)
                {
                    ctx.TotalInterestPaid = this.interestRateCalculateInput.MaxInterestPaid + 1;
                }

                return currentDelegate(ctx);
            };

            var actual = this.target.CalculateInterestRateByPaidInterest(this.interestRateCalculateInput, .02m, .2m);

            decimal expectedInterestRateThirdCalculation = .11m;

            Assert.IsTrue(actual.InterestRate.HasValue);
            Assert.AreEqual(expectedInterestRateThirdCalculation, actual.InterestRate.Value);
            Assert.IsNull(actual.Message);
        }

        [TestMethod]
        public void CalculateInterestRateByPaidInterest_GivenInput_Test_InterestRatesUsedInTheCalcuation()
        {
            var interestCalculator = this.Resolve<IOpeningBalanceCalculator>() as FakeCalculator;
            var currentDelegate = interestCalculator.ExecuteDelegate;
            int count = 0;
            List<decimal> interestRates = new List<decimal>();
            interestCalculator.ExecuteDelegate = (ctx) =>
            {
                interestRates.Add(ctx.Input.InterestRate);
                count++;
                // This simulates that the third calculation has the desired TotalInterestPaid
                if (count == this.interestRateCalculateInput.DurationOfLoan * 3)
                {
                    ctx.TotalInterestPaid = this.interestRateCalculateInput.MaxInterestPaid;
                }

                return currentDelegate(ctx);
            };

            var actual = this.target.CalculateInterestRateByPaidInterest(this.interestRateCalculateInput, .02m, .2m);

            decimal expectedInterestRateFirstCalculation = .02m;
            decimal expectedInterestRateSecondCalculation = .11m;
            decimal expectedInterestRateThirdCalculation = .155m;

            Assert.AreEqual(this.interestRateCalculateInput.DurationOfLoan * 3, interestRates.Count);

            bool unexpectedInterestRate;

            unexpectedInterestRate = interestRates
                .Take(this.interestRateCalculateInput.DurationOfLoan)
                .Any(a => a != expectedInterestRateFirstCalculation);
            Assert.IsFalse(unexpectedInterestRate, "Unexpected first interest rate");

            unexpectedInterestRate = interestRates
                .Skip(this.interestRateCalculateInput.DurationOfLoan)
                .Take(this.interestRateCalculateInput.DurationOfLoan)
                .Any(a => a != expectedInterestRateSecondCalculation);
            Assert.IsFalse(unexpectedInterestRate, "Unexpected second interest rate");

            unexpectedInterestRate = interestRates
                .Skip(this.interestRateCalculateInput.DurationOfLoan * 2)
                .Take(this.interestRateCalculateInput.DurationOfLoan)
                .Any(a => a != expectedInterestRateThirdCalculation);
            Assert.IsFalse(unexpectedInterestRate, "Unexpected third interest rate");
        }

        public ICalculator<MortgageCalculatorOperationContext> Resolve<T>()
        {
            Type type = typeof(T);
            bool found = this.dictionary.ContainsKey(type);
            if (!found)
            {
                string message = string.Format("Cannot resolve type '{0}'", type.Name);
                throw new KeyNotFoundException(message);
            }

            return this.dictionary[type];
        }

        private void PrepareDictionary()
        {
            this.dictionary[typeof(IInterestCalculator)] = this.CreateFakeCalculator<IInterestCalculator>(100m);
            this.dictionary[typeof(IPrincipalCalculator)] = this.CreateFakeCalculator<IPrincipalCalculator>(200m);
            this.dictionary[typeof(ITotalPaymentsCalculator)] = this.CreateFakeCalculator<ITotalPaymentsCalculator>(300m);
            this.dictionary[typeof(IClosingBalanceCalculator)] = this.CreateFakeCalculator<IClosingBalanceCalculator>(400m);
            this.dictionary[typeof(IOpeningBalanceCalculator)] = this.CreateFakeCalculator<IOpeningBalanceCalculator>(500m);
        }

        private FakeCalculator CreateFakeCalculator<T>(decimal initialValue)
        {
            var fakeCalculator = new FakeCalculator();
            fakeCalculator.Name = typeof(T).Name;
            fakeCalculator.ExecuteResult = CellOperationResult.CreateSucceed(initialValue);
            fakeCalculator.ExecuteDelegate = (ctx) =>
            {
                var result = fakeCalculator.ExecuteResult;
                fakeCalculator.ExecuteResult = CellOperationResult.CreateSucceed((decimal)fakeCalculator.ExecuteResult.Value + 1);
                return result;
            };

            return fakeCalculator;
        }
    }
}
